package za.org.jcicada.genss.alarm.impl;

import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import za.org.jcicada.genss.alarm.AlarmStatus;
import za.org.jcicada.genss.alarm.AlarmURN;
import za.org.jcicada.genss.alarm.IAlarm;
import za.org.jcicada.genss.alarm.IAlarmObserver;
import za.org.jcicada.genss.alarm.IAlarmRule;
import za.org.jcicada.genss.alarm.IAlarmSubject;
import za.org.jcicada.genss.messages.Result;
import za.org.jcicada.genss.messages.SuccessCode;
import za.org.jcicada.genss.sensor.ISensor;
import za.org.jcicada.genss.sensor.SensorURN;
import za.org.jcicada.genss.sensor.impl.SensorStatusValue;

/**
 * Implementation of IAlarmSubject.
 * 
 * @author rcrida
 */
public class AlarmSubject implements IAlarmSubject, Observer {

    class AlarmPlusRule {
        public Alarm alarm;
        public IAlarmRule rule;
        public AlarmPlusRule(Alarm alarm, IAlarmRule rule) {
            this.alarm = alarm;
            this.rule = rule;
        }
    }
    /**
     * Object used to create log messages
     */
    private static final Log logger = LogFactory.getLog(AlarmSubject.class);

    /**
     * The set of observers attached to this subject
     */
    private Set<IAlarmObserver> observers = new HashSet<IAlarmObserver>();

    /**
     * The complete set of alarm URIs that this subject knows about.
     */
    private Set<AlarmURN> alarmURNs = new HashSet<AlarmURN>();

    /**
     * A map between alarm URI and actual alarm object.
     */
    private Map<AlarmURN, AlarmPlusRule> alarmMap = new HashMap<AlarmURN, AlarmPlusRule>();

    /**
     * Map from sensor to alarmURI used for looking up URI given a sensor object
     * in the update method.
     */
    private Map<ISensor, AlarmURN> sensorAlarmURNs = new IdentityHashMap<ISensor, AlarmURN>();

    /**
     * Add a sensor. An alarm associated with this sensor will be to the alarm
     * rule set. This is used to create a mapping from the sensor to a
     * corresponding alarm URI.
     * 
     * @param sensorURN the URI for the sensor
     * @param sensor the sensor object for which alarms will be created.
     * @param rule the rule used to determine the alarm status on sensor updates
     */
    public synchronized void addSensor(SensorURN sensorURN, ISensor sensor, IAlarmRule rule) {
        try {
            AlarmURN alarmURN = new AlarmURN(sensorURN.getSystemURN(), sensor.getName());
            sensorAlarmURNs.put(sensor, alarmURN);
            Alarm alarm = new Alarm(AlarmStatus.CLEAR, alarmURN, null);
            alarmURNs.add(alarmURN);
            alarmMap.put(alarmURN, new AlarmPlusRule(alarm, rule));
            // finally attach to sensor for updates
            sensor.addObserver(this);
        } catch (URISyntaxException ex) {
            logger.error("This is a bug!", ex);
        }
    }
    
    /**
     * Stop the alarm subject. Just disconnects each observer and clears state
     */
    public void stop() {
        for (IAlarmObserver observer: new HashSet<IAlarmObserver>(observers)) {
            detachObserver(observer);
        }
        alarmURNs.clear();
        alarmMap.clear();
        sensorAlarmURNs.clear();
    }

    public IAlarm getAlarm(AlarmURN alarmURN) {
        AlarmPlusRule apr = alarmMap.get(alarmURN);
        return apr != null? apr.alarm: null;
    }

    /**
     * Called when a sensor is notified of a change in value. A new alarm status
     * is calculated and then suitable action is taken including: create new
     * alarm, update existing alarm or remove a cleared alarm.
     */
    public void update(Observable o, Object arg) {
        try {
            ISensor sensor = (ISensor) o;
            AlarmURN alarmURN = sensorAlarmURNs.get(o);
            AlarmPlusRule apr = alarmMap.get(alarmURN);
            if (arg instanceof SensorStatusValue) {
            	SensorStatusValue statusValue = (SensorStatusValue) arg;
            	AlarmStatus status = apr.rule.calcStatus(sensor, statusValue);
                apr.alarm.setStatus(status);
                apr.alarm.setMessage(statusValue.getMessage());
                notify(alarmURN, apr.alarm);
            }
        } catch (Exception ex) {
            logger.error("Failure during alarm update", ex);
        }
    }

    public synchronized void requestAttach(IAlarmObserver observer) {
        observers.add(observer);
        // Inform observer of current state of all alarms that are not CLEAR
        for (AlarmURN alarmURN : alarmMap.keySet()) {
            IAlarm alarm = alarmMap.get(alarmURN).alarm;
            if (alarm.getStatus() != AlarmStatus.CLEAR) {
                observer.informAlarm(alarmURN, alarm);
            }
        }
        // Reply to the attach request
        observer.replyAttach(new Result());
    }

    public synchronized void requestDetach(IAlarmObserver observer) {
        if (!observers.contains(observer)) {
            String message = "Specified observer not currently attached";
            observer.replyDetach(new Result(SuccessCode.COMPLETE_FAILURE, message));
            logger.error(message);
            return;
        }
        detachObserver(observer);
        observer.replyDetach(new Result());
    }

    public void requestAcknowledgeAlarm(IAlarmObserver observer, AlarmURN alarmURN) {
        IAlarm alarm = getAlarm(alarmURN);
        if (alarm != null) {
            alarm.acknowledge();
            observer.replyAcknowledgeAlarm(new Result(), alarmURN);
        } else {
            String message = String.format("Attempting to acknowledge unrecognized alarm: %s", alarmURN.toString());
            logger.warn(message);
            observer.replyAcknowledgeAlarm(new Result(SuccessCode.COMPLETE_FAILURE, message), alarmURN);
        }
    }

    public void requestAlarmSet(IAlarmObserver observer) {
        observer.replyAlarmSet(new Result(), new HashSet<AlarmURN>(alarmURNs));
    }

    public void requestReport(IAlarmObserver observer) {
        observer.replyReport(new Result(), "");
    }

    public void informDisconnect(IAlarmObserver observer) {
        detachObserver(observer);
    }

    /**
     * Detach the specified observer by removing references to it.
     * 
     * @param observer to be detached
     */
    private void detachObserver(IAlarmObserver observer) {
        observers.remove(observer);
    }

    /**
     * Notify all attached observers of the alarm.
     * 
     * @param alarmURN the alarm URI
     * @param alarm the alarm object
     */
    private void notify(AlarmURN alarmURN, IAlarm alarm) {
        String message = String.format("%s alarm %s with message: %s", alarm.getStatus(), alarm.getAlarmURN(), alarm.getMessage());
        switch (alarm.getStatus()) {
        case CLEAR:
            logger.info(String.format("Alarm %s has been CLEARed", alarm.getAlarmURN().toString()));
            break;
        case WARNING:
            logger.warn(message);
            break;
        case FUNCTIONAL:
            logger.error(message);
            break;
        case FAILURE:
            logger.fatal(message);
            break;
        }
        for (IAlarmObserver observer : observers) {
            observer.informAlarm(alarmURN, alarm);
        }
    }

}
